The Straight Framework

The Straight Framework (TSF) is a no-nonsense micro framework for PHP developers who appreciate minimalism and simplicity. The framework aims to be as straight forward as possible, hence the name.

The Straight Framework consists of:

Download The Straight Framework v 1.3 (2020 Edition) .
This package has been signed using Signify (Embedded Ed25519 Signature). Signify for Linux. Download and decompress Straight in a secure way like this:

$ curl https://gabordemooij.com/straight/straight-1.3.tgz | signify -Vz -p ./red.pub -t arc | tar xvzf -

Start the Straight Framework like this:

$ cd straight-1.3/public_html
$ php -S localhost:9000

Now visit localhost:9000 to see:

The Straight Way

TSF is an opinionated framework favouring simplicity over features. TSF has come into existence out of frustration with modern PHP frameworks. It is not just a framework but also a philosophy: coding is hard, we should therefore write as little as possible (avoid dependencies) and refrain from technical vanity.

request flow

Folder Structure

Your application lives in files and folders. As such, the project layout is like a home. It should be kept clean and tidy.

.
├── data
│   └── put_your_user_files_here.txt
├── public_html
│   ├── css
│   │   └── put_your_css_here.txt
│   ├── font
│   │   └── put_your_web_fonts_here.txt
│   ├── img
│   │   └── put_your_images_here.txt
│   ├── index.php
│   ├── js
│   │   └── put_your_javascript_here.txt
│   └── media
│       └── put_your_videos_here.txt
├── README.md
└── src
    ├── app
    │   ├── cli
    │   │   └── put_your_cli_scripts_here.txt
    │   ├── config
    │   │   └── settings.php
    │   ├── controller
    │   │   └── put_your_additional_controllers_here.txt
    │   ├── i18n
    │   │   └── en
    │   │       └── en.php
    │   ├── object
    │   │   └── put_your_models_or_classes_here.txt
    │   ├── script
    │   │   └── routes.php
    │   └── view
    │       └── welcome.php
    └── lib
        └── Straight
            └── straight.php

We assume all requests are handled by the index.php file in the web root. This file defines the paths to look up various parts of the system. After defining the path constants it loads routes.php file from your application folder: src/app/scripts/routes.php. The first thing your routing script should do is load the Straight Framework and map the requests using the fmap() function.

Constants

There is not much logic in the index.php file. It basically defines some path constants to make it easier to find stuff, then it loads PATH_SCRIPT /routes.php to map the request to a function. The index file defines constants that point to various locations:

PATH_SYSTEM:
system folder (not public)

PATH_SRC:
source code (/src)

PATH_DATA:
user data (/data)
for images for instance

PATH_APP:
application code
(/src/app)

PATH_LIB:
3rd party code
(/src/lib)

PATH_CONFIG:
configuration
(/src/app/config)

PATH_I18N:
translations
(/src/app/i18n)

PATH_VIEW:
templates
(/src/app/view)

PATH_VIEW:
templates
(/src/app/view)

PATH_MODEL:
PATH_OBJECT:
models/objects
(/src/app/object)

PATH_SCRIPT:
plain scripts
(/src/app/script)

Request Mapping

In TSF every URL will be mapped to its own explicitly named function by fmap(). This allows you to quickly look up the logic responsible for handling a specific request. The advantage of explicit functions is that your editor will lay them out neatly, like in the screenshot below:

End points in routing script, Geany.

Requests are mapped as follows:

__prefix_X_A_A..(B,B..);

Were __prefix is a user
defined prefix for
request handling functions.

Were X is the request method:
post or get.

A is an odd numbered URL segment.
B is an even numbered URL segment.

Examples:

GET /article/123
__url_get_article( 123 );

POST /catalogue/1/product/2
__url_post_catalogue_product( 1, 2 );

fmap() takes 2 arguments: an array of rewrite rules and a prefix to use for the functions. So, when deploying Straight in 'subfolder' you can use:

fmap( [ '/subfolder/' => '' ] );

If you wish to use a different prefix like '__myapp' use:

fmap( ..., '__myapp' );

TIP! fmap() also supports other HTTP methods like DELETE (__url_delete...). To use DELETE as your request method in an HTML form use:

<input type="hidden" name="_tunnel" value="DELETE">

Dictionary

The dict() function can be used to retrieve a setting, a text translation or something else. To set values, pass an array like this:

dict( ['database.host' => '127.0.0.1' ] );

to retrieve a value:

$hostname = dict( 'database.host' );

If you wish, you can pass additional arguments to the dictionary function:

<?= dict( '%d bottles of beer', [ 99 ] ); ?>

You can also add complex translations like this:

dict( ['%d bottles of beer' =>
 function($n) {
  switch($n) {
   case 0:
    return 'no beer';
    break;
   case 1:
    return '1 bottle of beer';
    break;
   default:
    return '%d bottles of beer';
    break;
  }
 }
] );

dict() can also return objects or arrays. So you can make dict() return configuration arrays or fully configured objects using dependency injection (if you like that kind of pattern).

Views

Being a framework for minimalists, TSF assumes you rely on PHP Alternative Syntax as being your template engine. To render a view:

view( 'finance/report', [ 'table' => $table ] );

This will load the PHP template in views/finance/report.php and make available the variable $table within the scope of that script. The variables $path and $vars are reserved. If you name variables after reserved variables they will be prefixed with __var_. Within your template you can escape user content to prevent XSS injections with esc():

<?= esc( $user_name ); ?>

The esc function will re-encode the content to UTF-8 (use esc(..,1) to skip re-encoding for better performance), filtering out any non-UTF-8 characters and encode the special characters like > < & single quotes and double quotes for UTF-8 documents. Note that this requires you to use UTF-8 and nothing else. To use UTF-8, start your document like this:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">

The view() function will not affect your output buffer. This means you need to organize your code properly, you can't send headers after having sent HTML to the browser. To display all errors on screen during development use:

ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(-1);

You can nest one view into another to reuse existing templates. If you wish to pass all variables in the current view to the next then pass $vars, like this:

view('subview', $vars);

CSRF

To protect yourself against Cross-Site Request Forgery (CSRF), a good first step would be to set the Same Site Cookie setting in your php.ini file to Strict.

session.cookie_samesite = "Strict"

JSON and Files

To write JSON to the output buffer use jout(). To output a file (image) use: flout( $path ). I recommend to use flout() to display uploaded images (stored outside the public folder, (/data) use PATH_DATA) to avoid accidental code injection/execution issues.

$x = ['status'=>'OK'];
jout( $x );

flout( PATH_DATA . '/x.jpg' );

Database

To interact with a relational, PDO-compatible database like MySQL, MariaDB, Postgres or SQLite you can use the query() function. The first time you call this function you need to pass a PDO-compatible object to be used from that point on:

$opts = [
 PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
 PDO::ATTR_EMULATE_PREPARES => false,
 PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
];
$dsn = "mysql:host=$host;dbname=$name;charset=utf8mb4";
$pdo = new PDO($dsn, $user, $pass, $opts);
query( $pdo );

Make sure you use utf8mb4 to ensure correct handling of UTF-8 characters. To test, see if you can get one of these unicode characters out of the database in one piece. Now you can use the query() function for all your queries:

$q = 'SELECT * FROM book WHERE id = ?';
$books = query($q, [ $id ]);

By default query() uses 'fetchAll' as its retrieval method. You can specify a different retrieval method using the 3rd parameter.

To create a database with the correct utf8mb4 character set in MariaDB/MySQL use:

CREATE DATABASE
mydatabase
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;

You might also want to take a look at RedBeanPHP ORM.

Tests

If you like to write automatic tests, you can use asrt(). Usage:

asrt( (1 + 1), 2, 'Simple!' );

asrt() will display the test number upon success and otherwise execution will stop and a debug stracktrace will de displayed (use on CLI only, not for public end points). If you pass a note, the test will display the note first.

Architecture

In TSF, a web application consists of a collection of scripts working closely together. Requests are dispatched to scripts using the router-script. The router-script as such acts as an extension of your web server, guiding the request to a script. A script is just a PHP template file containing both HTML and PHP code. For simple scripts you don't need anything more. If the application logic grows, you separate the template and the script by moving the logic into a separate file. The router will glue the two together. If you need to share the same logic between a number of scripts you create a class that can hold closely related and shared logic so you can reuse it. This class will reside in the object folder. Both the PATH_MODEL and PATH_OBJECT constants point to this folder. Whether you call your class instances objects or models is just a matter of style. If the wiring becomes complex, you can separate it from the routes and move it to a separate controller script. If you need helper functions and you wish to share those helpers between different projects, I recommend creating a folder named after your company (or after you personally) and put it in src/lib. This way you separate the reusable functions from the application specific ones. Automatic tests can help you prevent regressions. Depending on your style you can either put them in the cli folder or the scripts folder.